require 'rubygems'
require 'bit-struct'

require 'milfs/utils'

module Mil
  module Object

    OBJECT_TYPES = {
      "Mil::FS::Block"		=>    "01",
      "Mil::FS::BlockList"	=>    "02",
      "Mil::FS::FileNode"	=>    "03"
    }

    # Ruby representation of a binary structure using the BitStruct
    # library.
    #
    # ---
    # *Structure*:: <tt>ObjectStruct</tt> 
    # +address+:: <tt>Address of the block (SHA1 of *data)</tt>
    # +type+:: <tt>Type of storage object</tt>
    # +ref_count+:: <tt>Counter that tracks how many times this object has been referenced</tt>
    # +life_span+:: <tt>Optional field set when object will need to eventually be purged</tt>
    # +age+:: <tt>Age of the object, which is the epoch time when it was last accessed</tt>
    # +data+:: <tt>Variable length field containin the data segment of the Object</tt>
    class ObjectStruct < BitStruct
      char	    :address,	  320,	  "Address of this block (SHA1 of *data)"
      hex_octets    :type,	  8,	  "Type of Object"
      unsigned	    :ref_count,	  32,	  "Reference counter"
      float	    :life_span,	  32,	  "When set, is the remaining secs to self destruct"
      float	    :age,	  32,	  "Age of object last time it accessed, epoch time"
      rest	    :data,		  "Object Contents"

      def object
	object = nil

	OBJECT_TYPES.each_pair { |k,v|
	  object = eval "#{k}.new" if v == self.type
	}

	unless object.nil?
	  object.struct = self.data unless object.nil?

	  object
	else
	  raise RuntimeError, "unknown object type (#{self.type})"
	end
      end

      def age_object(max_age = nil)
	now = Time.now

	if max_age.nil?
	  max_age = ENV['MILFS_DEFAULT_LIFESPAN']
	end

	if @life_span == -1.0
	  @life_span = max_age
	end
	
	if @age == -1.0
	  @age = now.to_f
	else
	  aged = now.to_f - @age
	  life_span = @life_span - aged

	  @life_span = (life_span >= 0) ? life_span : 0
	  @age = now.to_f
	end
      end
    end

    # This class is the abstraction between the rest of the system
    # and the storage system off the participating node.
    class Store
      def initialize(path)
	begin
	  @path = path
	  @store = TokyoCabinetIO.new(@path)

	  true
	rescue Exception => e
	  false
	end
      end

      def store(object, age=false)
	unless object.is_a? Mil::Object::Interface
	  raise ArgumentError, "Expected a Mil::Object::Interface derived object, " +
		  "was passed #{object.class}"
	end

	type = OBJECT_TYPES["#{object.class}"]
	
	unless type
	  raise ArgumentError, "Unable to lookup object type for #{object.class}"
	end

	addr = Mil::Utils.to_address(object.struct.to_s)

	if @store.exist? addr
	  stored_object = ObjectStruct.new(@store.read(addr))

	  unless stored_object.object == object
	    raise RuntimeError, "Object content collision has occured, offending object " + 
			"is\n#{object.dump}\n\n and stored object is\n#{stored_object.object.dump}\n"
	  end

	  # If any of these are set, then the this is an aged object.
	  if (age) or (stored_object.age != -1.0) or (stored_object.life_span != -1.0)
	    # Ensure that the life span is already set, if not then set it.
	    # Need to ensure that MILFS_DEFAULT_LIFESPAN is set elsewhere!!
	    if age.is_a? Fixnum
	      stored_object.age_object(age)
	    else
	      stored_object.age_object
	    end
	  end

	  stored_object.ref_count = stored_object.ref_count + 1
	  object = stored_object
	else
	  new_object = ObjectStruct.new
	  new_object.address = addr
	  new_object.type = type
	  new_object.ref_count = 1
	  new_object.data = object.struct.to_s

	  if age
	    if age.is_a? Fixnum
	      new_object.age_object(age)
	    else
	      new_object.age_object
	    end
	  else
	    new_object.life_span = -1.0
	    new_object.age = -1.0
	  end

	  object = new_object
	end

	if object.is_a? ObjectStruct
	  pair = Mil::Object::KeyValue.new( :key => addr, :value => object.to_s )
	  @store.write(pair)
	else
	  raise RuntimeError, "An error has occurred resulting in a non-ObjectStruct being attempted to be stored"
	end

	addr
      end

      def fetch(addr)
	if @store.exist? addr
	  stored_object = ObjectStruct.new(@store.read(addr))
	else
	  raise ArgumentError, "Unable to find address (#{addr}) in the store."
	end

	stored_object.object
      end

      def exist?(addr)
	@store.exist? addr
      end

      def dereference(addr, age=false) 
	if @store.exist? addr
	  stored_object = ObjectStruct.new(@store.read(addr))

	  if stored_object.ref_count > 1
	    stored_object.ref_count = stored_object.ref_count - 1
	    
	    if age
	      if age.is_a? FixNum
		stored_object.age_object(age)
	      else
		stored_object.age_object
	      end
	    end

	  else
	    _remove(addr)
	  end
	else
	  raise ArgumentError, "Unable to find address (#{addr}) in the store."
	end

	true
      end

      alias_method :delete, :dereference

      private

      def _remove(addr)
	if @store.exist? addr
	  @store.delete addr
	else
	  raise  ArgumentError, "Unable to find address (#{addr}) in the store."
	end
      end
    end
  end
end
